/* @vitest-environment node */ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; import fs from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import fsp from "node:fs/promises"; vi.mock("@/lib/auth/session", () => ({ getSession: vi.fn(), })); import { getSession } from "@/lib/auth/session"; import { GET, dynamic, runtime } from "./route.js"; describe("GET /api/files/[branch]/[year]/[month]/[day]/[filename]", () => { let tmpRoot; const originalNasRoot = process.env.NAS_ROOT_PATH; const paramsOk = { branch: "NL01", year: "2024", month: "10", day: "23", filename: "test.pdf", }; beforeEach(async () => { vi.clearAllMocks(); tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "api-pdf-")); process.env.NAS_ROOT_PATH = tmpRoot; const dir = path.join(tmpRoot, "NL01", "2024", "10", "23"); await fs.mkdir(dir, { recursive: true }); await fs.writeFile(path.join(dir, "test.pdf"), "dummy-pdf-content"); }); afterEach(async () => { process.env.NAS_ROOT_PATH = originalNasRoot; if (tmpRoot) await fs.rm(tmpRoot, { recursive: true, force: true }); vi.restoreAllMocks(); }); it('exports dynamic="force-dynamic" (RHL-006)', () => { expect(dynamic).toBe("force-dynamic"); }); it('exports runtime="nodejs" (required for streaming)', () => { expect(runtime).toBe("nodejs"); }); it("returns 401 when unauthenticated", async () => { getSession.mockResolvedValue(null); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"), { params: Promise.resolve(paramsOk) } ); expect(res.status).toBe(401); expect(await res.json()).toEqual({ error: { message: "Unauthorized", code: "AUTH_UNAUTHENTICATED" }, }); }); it("returns 403 when branch access is forbidden", async () => { getSession.mockResolvedValue({ role: "branch", branchId: "NL01", userId: "u1", }); const res = await GET( new Request("http://localhost/api/files/NL02/2024/10/23/test.pdf"), { params: Promise.resolve({ ...paramsOk, branch: "NL02", }), } ); expect(res.status).toBe(403); expect(await res.json()).toEqual({ error: { message: "Forbidden", code: "AUTH_FORBIDDEN_BRANCH" }, }); }); it("returns 400 for non-pdf filename", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/test.txt"), { params: Promise.resolve({ ...paramsOk, filename: "test.txt", }), } ); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Only PDF files are allowed", code: "VALIDATION_FILE_EXTENSION", details: { filename: "test.txt" }, }, }); }); it("returns 400 for unsafe filename", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/foo/bar.pdf"), { params: Promise.resolve({ ...paramsOk, filename: "foo/bar.pdf", }), } ); expect(res.status).toBe(400); expect(await res.json()).toEqual({ error: { message: "Invalid filename parameter", code: "VALIDATION_FILENAME", details: { filename: "foo/bar.pdf" }, }, }); }); it("returns 404 when the PDF does not exist (authorized)", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/missing.pdf"), { params: Promise.resolve({ ...paramsOk, filename: "missing.pdf", }), } ); expect(res.status).toBe(404); expect(await res.json()).toEqual({ error: { message: "Not found", code: "FS_NOT_FOUND", details: { branch: "NL01", year: "2024", month: "10", day: "23", filename: "missing.pdf", }, }, }); }); it("returns 500 for other filesystem errors (mocked)", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const spy = vi .spyOn(fsp, "stat") .mockRejectedValue( Object.assign(new Error("EACCES"), { code: "EACCES" }) ); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"), { params: Promise.resolve(paramsOk) } ); expect(res.status).toBe(500); expect(await res.json()).toEqual({ error: { message: "Internal server error", code: "FS_STORAGE_ERROR" }, }); spy.mockRestore(); }); it("streams the PDF with inline Content-Disposition by default", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const res = await GET( new Request("http://localhost/api/files/NL01/2024/10/23/test.pdf"), { params: Promise.resolve(paramsOk) } ); expect(res.status).toBe(200); expect(res.headers.get("Content-Type")).toBe("application/pdf"); expect(res.headers.get("Cache-Control")).toBe("no-store"); expect(res.headers.get("Content-Disposition")).toBe( 'inline; filename="test.pdf"' ); const buf = await res.arrayBuffer(); expect(Buffer.from(buf).toString("utf8")).toBe("dummy-pdf-content"); }); it("uses attachment disposition when download=1", async () => { getSession.mockResolvedValue({ role: "admin", branchId: null, userId: "u2", }); const res = await GET( new Request( "http://localhost/api/files/NL01/2024/10/23/test.pdf?download=1" ), { params: Promise.resolve(paramsOk) } ); expect(res.status).toBe(200); expect(res.headers.get("Content-Disposition")).toBe( 'attachment; filename="test.pdf"' ); }); });